/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package core;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import logging.LOG;

/**
 *
 * @author Jorge
 */
public class ProtocolSD {


    public static final byte SUCCESS =1;
    public static final byte ERROR =0;

    

    public enum Operation {
        GET,
        REDIRECT_READ,
        REDIRECT_WRITE,
        PUT_KEYS,
        PUT_DATA,
        DATA,
        DONE,
        ERROR,
        TICKET,
        COMMIT,
        NOTIFY,
        REMOVE,
        STATUS,
        KEYSET
    }
    private static int HEADER_SIZE = 2;
    private static int INT_SIZE = 4;
    /** Buffer temporário de leitura*/
    protected ByteBuffer buffer = ByteBuffer.allocate(1024);
    /** Lista de parametros de saida*/
    private List<byte[]> out = new ArrayList<byte[]>(10);
    /** Tamanho do parametro actual */
    private int tmpTotal = 2;
    /** Tamanho lido do parametro actual */
    private int tmpParcial = 0;
    /** Lista de buffers para leitura de parametros*/
    private List<byte[]> tmpBuffers = new ArrayList<byte[]>();
    /** listener associado ao protocolo*/
    private IProtocolListener listener;
    /** número de argumentos lidos */
    private int numArgs = 0;
    /** número total de argumentos*/
    private int numTotalArgs = 0;
    private SelectionKey key;
    private SocketChannel channel;
    private List<ProtocolSD.Operation> unJoinParam=new LinkedList<Operation>();
    private Operation operation;

    private FileChannel fileChannel;
    private int fileSize=0;


    private enum STATE {
        READ_HEADER,
        READ_PARAM,
        READ_PARAM_SIZE
    }
    private STATE state = STATE.READ_HEADER;

    public ProtocolSD(IProtocolListener listener, SelectionKey key) {
        this.listener = listener;
        this.key = key;
        channel = (SocketChannel) this.key.channel();
    }

    public void setUnJoinParam(List<Operation> unJoinParam) {
        this.unJoinParam = unJoinParam;
    }

   

    

    public void setListener(IProtocolListener listener) {
        this.listener = listener;
    }

    public IProtocolListener getListener() {
        return listener;
    }

    public SelectionKey getKey() {
        return key;
    }

    public void close() throws IOException {
        key.cancel();
        channel.close();
    }

    protected void handlerRead() throws IOException {
        int numTotal = 0;
        int position = 0;
        do {
            buffer.clear();
            numTotal = channel.read(buffer);
            buffer.flip();
            if (numTotal == -1) {
                this.close();
            } else {
                if (numTotal != 0) {
                    read(buffer, position, numTotal);
                }
            }
        } while (numTotal != 0 && numTotal != -1);
    }

    public static byte[] getBytes(ByteBuffer bb, int position, int offset) {
        byte b[] = new byte[offset];
        for (int i = 0; i < offset; i++) {
            b[i] = bb.get(i + position);
        }
        return b;
    }

    public static byte[] intToByte(int value) {
        return new byte[]{
                    (byte) (value >>> 24),
                    (byte) (value >>> 16),
                    (byte) (value >>> 8),
                    (byte) value
                };
    }

    public static int byteArrayToInt(byte[] b, int offset) {
        int value = 0;
        for (int i = 0; i < 4; i++) {
            int shift = (4 - 1 - i) * 8;
            value += (b[i + offset] & 0x000000FF) << shift;
        }
        return value;
    }

    public static long byteArrayToLong(byte[] b, int offset) {
        byte [] bs = subArray(b, offset, b.length);
        ByteBuffer buffer = ByteBuffer.allocate(9);
        buffer.put(bs);
        buffer.flip();
        return buffer.getLong();
    }

    protected void read(ByteBuffer buffer, int position, int remaning) {
        while (remaning > 0) {
            if (remaning >= tmpTotal - tmpParcial) {        //Consegue acabar de ler o parametro com o buffer actual
                int readBytes = tmpTotal - tmpParcial;      //Número de bytes a ler do buffer para completar o parametro
                byte[] readedBytes = getBytes(buffer, position, readBytes);

                if (state != STATE.READ_PARAM || !unJoinParam.contains(operation)) {
                    tmpBuffers.add(readedBytes); //Adicionar ao buffer temporário os bytes para completar o parametro
                    byte[] param = joinBuffer(tmpBuffers, tmpTotal);      //junta os varios buffer para criar o parametro
                    if (state == STATE.READ_PARAM_SIZE) {
                        int size = byteArrayToInt(param, 0);

                        listener.onReadArgumentSize(size, this);
                        state = STATE.READ_PARAM;
                        initBuffer(size);
                    } else {
                        if (state == STATE.READ_PARAM) {
                            
                            listener.onReadArgument(param, this);
                            numArgs++;
                            if (numArgs == numTotalArgs) {
                               
                                listener.onReadAllArguments(this);
                                state = STATE.READ_HEADER;
                                numArgs = 0;
                                initBuffer(HEADER_SIZE);
                            } else {
                                state = STATE.READ_PARAM_SIZE;
                                initBuffer(INT_SIZE);
                            }
                        } else {
                            numTotalArgs = param[1];
                            operation= Operation.values()[param[0]];
                            listener.onReadHeader(param[0], numTotalArgs, this);
                            if(numTotalArgs==0){
                                listener.onReadAllArguments(this);
                                state = STATE.READ_HEADER;
                                initBuffer(HEADER_SIZE);
                                
                            }else{
                                state = STATE.READ_PARAM_SIZE;
                                initBuffer(INT_SIZE);
                            }
                        }
                    }
                } else {
                    listener.onReadParcialParam(readedBytes, tmpParcial , readBytes,this); // lança evento parcial
                    numArgs++;
                    
                    if (numArgs == numTotalArgs) {
                        listener.onReadAllArguments(this);
                        state = STATE.READ_HEADER;
                        numArgs = 0;
                        initBuffer(HEADER_SIZE);
                    } else {
                        state = STATE.READ_PARAM_SIZE;
                        initBuffer(INT_SIZE);
                    }
                }

                    position += readBytes;
                    remaning -= readBytes;

            } else {
                byte [] readedBytes= getBytes(buffer, position, remaning);
                if(state != STATE.READ_PARAM || !unJoinParam.contains(operation)){
                    tmpBuffers.add(readedBytes);
                }else{
                    listener.onReadParcialParam(readedBytes, tmpParcial , remaning,this); // lança evento parcial
                }
                tmpParcial += remaning;
                remaning=0;
            }
        }
    }

    protected void initBuffer(int size) {
        tmpBuffers.clear();
        tmpTotal = size;
        tmpParcial = 0;
    }

    public void setFileChannel(FileChannel fileChannel,int size) {
        this.fileChannel = fileChannel;
        this.fileSize = size;
    }

   

    

    

    protected byte[] converter(ByteBuffer buffer, int min, int max) {
        byte[] bs = new byte[max - min];
        for (int i = 0; i < max - min; i++) {
            bs[i] = buffer.get(i + min);
        }
        return bs;
    }

    public  static byte[] joinBuffer(List<byte[]> bs, int total) {
        int i = 0;
        byte[] bs1 = new byte[total];
        for (byte[] bs12 : bs) {
            for (byte b : bs12) {
                bs1[i++] = b;
            }
        }
        return bs1;
    }

    public static byte[] subArray(byte []aux,int min,int max){
         byte [] dataAux = new byte[max-min];
            for(int i=min ; i<max;i++){
                dataAux[i-min]=aux[i];
            }
         return dataAux;
    }

    public void handlerWrite() throws IOException {

        if (out.size() == 0) {
            if(fileChannel==null || fileChannel.position() == fileChannel.size()){
                key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
                fileChannel=null;
                listener.onWriteComplete(this);
               
                return;
            }else{
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                int num = fileChannel.read(byteBuffer);
                byteBuffer.flip();
                channel.write(byteBuffer);
                return;
            }
            

        }
        byte[] bs = out.remove(0);
        ByteBuffer buffer = ByteBuffer.allocate(bs.length);
        buffer.clear();
        buffer.put(bs);
        buffer.flip();
        int num = channel.write(buffer);
        if(num!=bs.length){
            out.add(0,subArray(bs, num, bs.length));
        }

    }

    private String byteToStr(byte [] bs){
           StringBuilder builder = new StringBuilder();
           for(byte b : bs){
                builder.append((int)b+" ");
           }
           return builder.toString();
    }

    public void write(byte b) {
        write(new byte[]{b});
    }

    public void write(byte operation, int... params) {
        ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE + 2 * params.length * INT_SIZE);
        buffer.put(operation);
        buffer.put((byte) params.length);
        for (int i : params) {
            buffer.putInt(INT_SIZE);
            buffer.putInt(i);
        }
        buffer.flip();
        write(buffer.array());
    }

    public void write(int key, FileChannel channel) throws IOException {
        writeInt(key);
        int num = 0;
        do {
            buffer.clear();
            num = channel.read(buffer);
            buffer.flip();
            write(buffer.array());
        } while (num != 0 && num != -1);

    }

    public void write(byte operation, List<Integer> params) {
        ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE + 2 * params.size() * INT_SIZE);
        buffer.put(operation);
        buffer.put((byte) params.size());
        for (Integer integer : params) {
            buffer.putInt(INT_SIZE);
            buffer.putInt(integer);
        }
        buffer.flip();
        write(buffer.array());
    }

    public void write(byte operation, List<Integer> params, List<Integer> params1) {
        ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE + 3*params.size() * INT_SIZE);
        buffer.put(operation);
        buffer.put((byte) params.size());
        int i = 0;
        for (Integer integer : params) {
            buffer.putInt(2*INT_SIZE);
            buffer.putInt(integer);
            buffer.putInt(params1.get(i++));
        }
        buffer.flip();
        write(buffer.array());
    }

    public void writeInt(byte b[], List<Integer> list) {
        ByteBuffer buffer = ByteBuffer.allocate((5 + INT_SIZE)*list.size());
        int i = 0;
        for (Integer integer : list) {
            buffer.putInt(1+INT_SIZE);
            buffer.put(b[i++]);
            buffer.putInt(integer);
        }
        buffer.flip();
        write(buffer.array());
    }

    public void writeLong(byte b[], List<Long> list) {
        ByteBuffer buffer = ByteBuffer.allocate((5 + 8)*list.size());
        int i = 0;
        for (Long integer : list) {
            buffer.putInt(1+8);
            buffer.put(b[i++]);
            buffer.putLong(integer);
        }
        buffer.flip();
        write(buffer.array());
    }

    public void writeLong(Long long1){
       ByteBuffer buffer = ByteBuffer.allocate(8);
       buffer.putLong(long1);
       buffer.flip();
       write(buffer.array());
    }

    public void writeString(byte b[], List<String> list) {
        int i = 0;
        for (String st : list) {
            writeInt(1+st.length());
            write(b[i++]);
            write(st.getBytes());
        }
    }

    public void write(byte operation, String... strs) {
        write(new byte[]{operation, (byte) strs.length});
        for (String string : strs) {
            writeString(string);
        }
    }

    public void writeStrings(byte operation, List<String> strs) {
        write(new byte[]{operation, (byte) strs.size()});
        for (String string : strs) {
            writeString(string);
        }
    }

    public void writeString(String string) {
        byte[] bs = string.getBytes();
        writeInt(bs.length);
        write(bs);
    }

    public void writeInt(int i) {

        write(intToByte(i));
    }

    public synchronized void write(byte[] bs) {
        if (out.size() == 0) {
            
            key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
        }
        out.add(bs);
    }



    public interface IProtocolListener {

        public void onReadHeader(int operation, int numArgs, ProtocolSD protocolSD);

        public void onReadArgument(byte[] data, ProtocolSD protocolSD);

        public void onReadArgumentSize(int size, ProtocolSD protocolSD);

        public void onReadAllArguments(ProtocolSD protocolSD);

        public void onWriteComplete(ProtocolSD protocolSD);

        public void setCallBack(IServerCallBack callback);

        public IServerCallBack getCallBack();

        public void onReadParcialParam(byte[] readedBytes, int offset, int limit,ProtocolSD protocolSD);

        public void onAcceptConnect();

        public boolean isClose();

    }
}
